﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Flight
{
    class Airport
    {
        /* an airport is a graph of airport-nodes, which are either:
         * - a taxiway node
         * - a runway node
         * - a parking node
         * 
         * an airport also contains a list of polygonal areas which
         *   are filled with the taxiway-color
         * it also has a list of objects (models) to draw
         *   at certain locations
         */

        public enum NodeType
        {
            PARKING,
            TAXIWAY,
            RUNWAY
        }
        public class Node
        {
            public NodeType type;
            public Vector2 loc;
            public List<Node> connections;

            public Node(NodeType type, Vector2 loc)
            {
                this.type = type;
                this.loc = loc;
                this.connections = new List<Node>();
            }

            public void connectTo(Node other)
            {
                connections.Add(other);
            }

            public string getSaveFileLine(Dictionary<Node, int> indices)
            {
                string typeStr;
                switch (type)
                {
                    case NodeType.PARKING: typeStr = "PRK"; break;
                    case NodeType.RUNWAY: typeStr = "RWY"; break;
                    case NodeType.TAXIWAY: typeStr = "TWY"; break;
                    default: throw new Exception("Forgot something...");
                }

                string text = typeStr + " " + loc.X + " " + loc.Y;
                foreach (Node neighbor in connections)
                    text += " " + indices[neighbor];

                return text;
            }
        }
        class Segment
        {
            public Node a, b;

            public Segment(Node a, Node b)
            {
                this.a = a;
                this.b = b;
            }

            public override bool Equals(object obj)
            {
                if (!(obj is Segment)) return obj.Equals(this);
                Segment s = (Segment)obj;

                return (a == s.a && b == s.b) 
                    || (a == s.b && b == s.a);
            }

            public override int GetHashCode()
            {
                return a.GetHashCode() + b.GetHashCode();
            }
        }

        bool frozen;
        List<Node> nodes;
        List<Polygon> filledAreas;
        HashSet<Segment> taxiwaySegments;
        HashSet<Segment> runwaySegments;

        public Airport()
        {
            this.nodes = new List<Node>();
            this.filledAreas = new List<Polygon>();
            this.frozen = false;
        }
        public Airport(IEnumerable<Node> nodes, IEnumerable<Polygon> filledAreas)
        {
            this.nodes = new List<Node>(nodes ?? empty<Node>());
            this.filledAreas = new List<Polygon>(filledAreas ?? empty<Polygon>());
            this.frozen = false;
        }

        public void freeze()
        {
            if (frozen)
                throw new InvalidOperationException("Airport is already frozen.");
            checkValid();
            taxiwaySegments = new HashSet<Segment>();
            runwaySegments = new HashSet<Segment>();
            foreach (Node node in nodes)
            {
                foreach (Node neighbor in node.connections)
                {
                    if (node.type == NodeType.RUNWAY && neighbor.type == NodeType.RUNWAY)
                        runwaySegments.Add(new Segment(node, neighbor));
                    else taxiwaySegments.Add(new Segment(node, neighbor));
                }
            }
            frozen = true;
        }

        void checkValid()
        {
            // each RUNWAY may be connected to exactly 1 or 2 other RUNWAY(s), and
            //   may be connected to any number of TAXIWAYs
            // there may be no loop of RUNWAYs
            // each PARKING must be connected to a single TAXIWAY
            // the entire airport must be connected
            // every node must be connected to at least one other node

        }

        public string[] getSaveFileText()
        {
            /* format is:
             * n {number of nodes}
             * {n times}:
             *   node-type x y connection-indices
             * m {number of filled areas}
             * { m times}:
             *   {p times} x y {p = number of points in polygon}
             * for example: a square with a diagonal line connected
             *   and a single filled area would be:
             *   
             *   4
             *   RWY -1 -1 1 3
             *   RWY 1 -1 0 2 3
             *   TWY 1 1 1 3
             *	 TWY 1 -1 0 1 2
             *	 1
             *	 -0.5 -0.5 0.5 -0.5 0.5 0.5 -0.5 0.5
             *
             * 0 ---- 1
             * |     /|
             * | /--/ |
             * |/     |
             * 3 ---- 2
             */

            int index = 0;
            Dictionary<Node, int> indices = new Dictionary<Node, int>();
            foreach (Node node in nodes)
                indices[node] = index++;

            List<string> lines = new List<string>();
            lines.Add(nodes.Count.ToString());
            foreach (Node node in nodes)
                lines.Add(node.getSaveFileLine(indices));

            return lines.ToArray();
        }
        static NodeType nodeTypeFromStr(string str)
        {
            switch (str)
            {
                case "RWY": return NodeType.RUNWAY;
                case "TWY": return NodeType.TAXIWAY;
                case "PRK": return NodeType.PARKING;
                default: throw new ArgumentException("'" + str + "' is not a valid node type.");
            }
        }
        // parses and returns a _frozen_ airport
        public static Airport fromSaveFileText(string[] lines)
        {
            // line count/info
            //            0: first line: n
            //       [1, n]: next n lines: node definitions
            //          n+1: next line: f
            // [n+2, n+2+f]: next f lines: filled area definitions

            // argument checking
            if (lines.Length == 0)
                throw new ArgumentException("Must have at least one line.");
            // get number of nodes
            int n;
            if (!int.TryParse(lines[0], out n))
                throw new ArgumentException("Expected node count on first line.");
            if (lines.Length < n + 1)
                throw new ArgumentException("Expected " + n + " node definition" + (n == 1 ? "" : "s") + ".");
            // split into tokens
            string[][] parts = new string[n][];
            for (int i = 0; i < parts.Length; i++)
            {
                parts[i] = lines[i + 1].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                if (parts[i].Length < 3)
                    throw new ArgumentException("Node definition #" + i + " must have at least three tokens.");
            }
            // first pass through nodes: basic data
            Node[] nodes = new Node[parts.Length];
            for (int i = 0; i < nodes.Length; i++)
            {
                string[] line = parts[i];
                NodeType type = nodeTypeFromStr(line[0]);
                float x, y;
                if (!float.TryParse(line[1], out x) || !float.TryParse(line[2], out y))
                    throw new ArgumentException("Could not parse node location for node #" + i + ".");
                nodes[i] = new Node(type, new Vector2(x, y));
            }
            // second pass through nodes: connections
            for (int i = 0; i < nodes.Length; i++)
            {
                string[] line = parts[i];
                for (int c = 0; c < line.Length - 3; c++)
                {
                    int neighborIndex;
                    if (!int.TryParse(line[c + 3], out neighborIndex))
                        throw new ArgumentException("Could not parse neighbor index #" + c + " for node #" + i + ".");
                    if (neighborIndex < 0 || neighborIndex >= nodes.Length)
                        throw new ArgumentException("Neighbor index #" + c + " for node #" + i + " is out of range.");
                    nodes[i].connectTo(nodes[neighborIndex]);
                }
            }

            // filled areas
            if (lines.Length < n + 2)
                throw new ArgumentException("Expected number of filled areas after node definitions.");
            int f; // number of filled areas
            if (!int.TryParse(lines[n + 1], out f))
                throw new ArgumentException("Expected number of filled areas after node definitions.");
            if (lines.Length < 1 + n + 1 + f)
                throw new ArgumentException("Expected " + f + " filled area definitions.");
            Polygon[] polys = new Polygon[f];
            for (int i = 0; i < f; i++)
            {
                string[] points = lines[1 + n + 1 + i].Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
                if (points.Length % 2 != 0)
                    throw new Exception("Expected even number of tokens for filled area #" + i + ".");
                if (points.Length / 2 < 3)
                    throw new Exception("Filled area #" + i + " must have at least 3 vertices.");
                Vector2[] vertices = new Vector2[points.Length / 2];
                for (int v = 0; v < vertices.Length; v++)
                {
                    float x, y;
                    if (!float.TryParse(points[2 * v], out x) || !float.TryParse(points[2 * v + 1], out y))
                        throw new ArgumentException("Could not parse vertex location for filled area #" + i + ".");
                    vertices[v] = new Vector2(x, y);
                }
                polys[i] = new Polygon(vertices);
            }

            Airport airport = new Airport(nodes, polys);
            airport.freeze();
            return airport;
        }
        static IEnumerable<T> empty<T>() { yield break; }

        Vector2 center;

        public void drawAt(float x, float y)
        {
            if (!frozen)
                throw new InvalidOperationException("Airport must be frozen.");

            center = new Vector2(x, y);

            // must draw in 3 layers:
            //   filled areas first,
            //   then taxiways,
            //   then runways over taxiways
            drawFilledAreas();
            drawTaxiways();
            drawRunways();
        }

        private void rectBetween(Vector2 p1, Vector2 p2, Color color, float width, float offset)
        {
            Graphics.line(color, width, offset, 
                p1 + center, p2 + center);
        }
        private void rectBetween(Node a, Node b, Color color, float width, float offset)
        {
            rectBetween(a.loc, b.loc, color, width, offset);
        }

        readonly Color taxiwayColor = new Color(120, 120, 120);
        private void drawFilledAreas()
        {
            foreach (Polygon poly in filledAreas)
            {
                Graphics.polygon(taxiwayColor, poly, center);
            }
        }
        private void drawTaxiways()
        {
            // must draw in 2 layers:
            //   background taxiway (gray) area first,
            //   taxiway lines over taxiway paths
            foreach (Segment segment in taxiwaySegments)
            {
                rectBetween(segment.a, segment.b, taxiwayColor, 20, 0);
            }
            foreach (Segment segment in taxiwaySegments)
            {
                // side lines
                rectBetween(segment.a, segment.b, new Color(255, 200, 0), 1.25f, -8);
                rectBetween(segment.a, segment.b, new Color(255, 200, 0), 1.25f, +8);
                // centerline
                rectBetween(segment.a, segment.b, new Color(255, 200, 0), 1f, 0);
            }
        }
        private void drawRunways()
        {
            Color backcolor = new Color(30, 30, 30);

            // must draw in 2 layers:
            //   background runway area (black/dark gray) first,
            //   then runway markings/lines over paved area
            foreach (Segment segment in runwaySegments)
            {
                rectBetween(segment.a, segment.b, backcolor, 50, 0);
            }
            foreach (Segment segment in runwaySegments)
            {
                // side lines
                rectBetween(segment.a, segment.b, Color.White, 2f, -23);
                rectBetween(segment.a, segment.b, Color.White, 2f, +23);
                // centerline
                rectBetween(segment.a, segment.b, Color.White, 1.5f, 0);
            }
        }
    }
}
